


var PACKET_SIZE = 64 * 1024;
var BLOCK_RCVR_PORT = 22;
var BLOCK_ACK_PORT = 23;
var PACKET_ACK_WINDOW = 2;
var PACKET_ACK_TIMEOUT = 2.0; // seconds


/*************************************************************/

var RPC = function (host, app, serviceIP, serviceId) {
	this.host = host;
	this.app = app;
	this.serviceIP = serviceIP;
	this.serviceId = serviceId;
};

RPC.prototype.rpc = function (fn, data, size, callbackfn, errfn) {
	if (this.busy) {
		errfn.call();
	}
	
	this.fn = fn;
	this.data = data;
	this.size = size;
	this.callbackfn = callbackfn;
	this.errfn = errfn;
	
	this.host.network.send(
		this.host.ip, 
		Constants.RPC.RPC_PORT, 
		this.serviceIP,
		Constants.RPC.RPC_PORT,
		this,
		size);
};

/*************************************************************/
/**
 *  fn(senderIP, senderPort, data, size)
 *  errfn(errcode, destIP, destPort, data, size)
 */
var Socket = function (host, app, port, fn, errfn) {
	this.host = host;
	this.app = app;
	this.port = port;
	this.fn = fn;
	this.errfn = errfn;
};

Socket.prototype.sendto = function (destIP, destPort, data, size) {
	this.host.network.send(this.host.ip, this.port, destIP, destPort, data, size);
};

Socket.prototype.connect = function (destIP, destPort) {
	this.destIP = destIP;
	this.destPort = destPort;
};

Socket.prototype.send = function (data, size) {
	if (!this.destIP) {
		print("no destip " + this.destIP);
		if (this.errfn) {
			this.errfn.call(this.app, Constants.Socket.NOT_CONNECTED, null, null, data, size);
		}
		return;
	}
	
	this.sendto(this.destIP, this.destPort, data, size);
};

/*************************************************************/


var Host = {
	start: function () {
		this.hd = new Disk(IDE, this);
		this.freePort = 1024;
	},
	
	addApplication: function () {
		return this.sim.addEntity.apply(this.sim, arguments);
	},
	
	shutdown: function () {
		
	},
	isAlive: function () {
		
	},
	socket: function (app, port, fn, errfn) {
		// If no network, report error and return
		if (!this.network) {
			return errfn && errfn.call(app, Constants.Socket.NO_NETWORK);
		}

		if (!this.socketList) {
			this.socketList = {};
		}
		
		// if another application is already registered at this port, report error and return
		if (this.socketList[port]) {
			return errfn && errfn.call(app, Constants.Socket.PORT_TAKEN);
		}

		if (port === 0) {
			port = this.freePort ++;
		}
		
		// Create a new socket
		var sock = new Socket(this, app, port, fn, errfn);
		this.socketList[port] = sock;
		return sock;
	},
	
	registerService: function (app, serviceId) {
		ARG_CHECK(arguments, 2, 2);
		if (!this.network) {
			return null;
		}
		if (!this.serviceList) {
			this.serviceList = {};
		}
		
		if (this.serviceList[serviceId]) {
			return null;
		}
		
		this.serviceList[serviceId] = app;
	},
	
	connectService: function (app, serviceIP, serviceId) {
		ARG_CHECK(arguments, 3, 3);
		return new RPC(this, app, serviceIP, serviceId);
	},

	onRpcData: function (senderIP, msg, size) {
		ARG_CHECK(arguments, 3, 3);
		if (senderIP !== msg.serviceIP) {
			if (!this.serviceList) {
				return msg.errfn && 
					msg.errfn.call(msg.app, Constants.RPC.SERVICE_UNREACHABLE, msg.data, msg.size);
			}

			var service = this.serviceList[msg.serviceId];
			if (!service) {
				return msg.errfn &&
					msg.errfn.call(msg.app, Constants.RPC.SERVICE_UNREACHABLE, msg.data, msg.size);
			}
			
			if (!msg.fn) {
				return msg.errfn &&
					msg.errfn.call(msg.app, Constants.RPC.METHOD_UNREACHABLE, msg.data, msg.size);
			}

			var result = msg.fn.call(service, senderIP, msg.data, size);
			
			if (result) {
				ARG_CHECK(result, 2, 2); // check result is an array of size 2
				msg.data = result[0];
				msg.size = result[1];

				this.network.send(
					this.ip, 
					Constants.RPC.RPC_PORT, 
					senderIP, 
					Constants.RPC.RPC_PORT, 
					msg,
					msg.size);
				}
		} else {
			msg.callbackfn.call(msg.app, msg.data, msg.size);
		}
	},
	
	onData: function (senderIP, senderPort, destPort, data, size) {
		if (destPort == Constants.RPC.RPC_PORT) {
			// This is RPC message
			this.onRpcData(senderIP, data, size);
			return;
		}
		
		// this is socket message
		if (!this.socketList) {
			var msg = [Constants.Socket.PORT_UNREACHABLE, senderIP, senderPort, destPort, data, size];
			var sender = this.network.resolve(senderIP);
			//this.send(msg, 0, sender, sender.onDataError);
			return;
		}
		var sock = this.socketList[destPort];
		if (!sock) {
			var msg = [Constants.Socket.PORT_UNREACHABLE, senderIP, senderPort, destPort, data, size];
			var sender = this.network.resolve(senderIP);
			this.send(msg, 0, sender, sender.onDataError);
			return;
		}
		
		sock.fn.call(sock.app, senderIP, senderPort, data, size);
	},
	
	onDataError: function (msg) {
		if (!this.socketList) { return; }
		var sock = this.socketList[senderPort];
		if (!sock) { return; }
		sock.errfn.apply(sock.app, msg);
	}
};

var Cloud = {
	ipAddress: 100001,
	start: function () {
		this.sim = new Sim();
	},
	newHost: function () {
		ARG_CHECK(arguments, 0, 0);
		var host = this.sim.addEntity(Host);
		host.ip = this.ipAddress++;
		return host;
	},
	newSwitch: function (ports, speed) {
		ARG_CHECK(arguments, 2, 2);
		return new Switch(ports, speed);
	},
	connect: function (host, sw, port) {
		ARG_CHECK(arguments, 3, 3, null, Switch);
		sw.connect(host, host.ip, port);
		host.network = sw;
	},
	setLogger: function (writer) {
		ARG_CHECK(arguments, 1, 1);
		this.sim.setLogger(writer);
	},
	log: function (msg) {
		this.sim.log(msg);
	},
	simulate: function (duration) {
		ARG_CHECK(arguments, 1, 1);
		this.sim.simulate(duration);
	},
	
	assert: function (cond, msg, entity) {
		if (!cond) {
			if (entity) entity.log(msg);
			else this.sim.log(msg);
			throw new Error("Assertion error");
		}
	}
};
